#include "pch.hpp"
#include "D3D11Device.hpp"
#include "D3D11Exception.hpp"




Engine::Video::D3D11Device::D3D11Device(const EngineInfo& info)
	: m_logger(info.serviceProvider->service<logger::stream>())
	, m_info(info)
	, m_videoAdapter(info.graphicsAdapter)
	, m_displayMode(info.displayMode)

	, m_factory(createFactory())

	, m_camera(info)
{
	m_logger->log_debug("Creating video device: D3D11");

	//
	// Device creation
	//

	switch(info.displayMode.windowMode)
	{
	case WindowMode::Fullscreen: m_logger->log_info(QString("Creating hardware device: fullscreen, %1x%2 px").arg(info.displayMode.width).arg(info.displayMode.height)); break;
	case WindowMode::Windowed:   m_logger->log_info(QString("Creating hardware device: windowed, %1x%2 px").arg(info.displayMode.width).arg(info.displayMode.height)); break;
	default:                     BOOSTEXT_THROW(invalid_value_exception(info.displayMode.windowMode));
	}

	IDXGIAdapterPtr adapter = adapterById(m_factory, info.graphicsAdapter.index);
	DXGI_SWAP_CHAIN_DESC sd = createDeviceDesc();
	uint32 flags = deviceFlags();

	IDXGISwapChain* sc = NULL;
	ID3D11Device* device = NULL;
	ID3D11DeviceContext* ctx = NULL;
	D3D_FEATURE_LEVEL featureLevel = highestFeatureLevel(info.graphicsAdapter);
	D3D11_API(D3D11CreateDeviceAndSwapChain(adapter.get() /*NULL*/,
		D3D_DRIVER_TYPE_UNKNOWN /*D3D_DRIVER_TYPE_REFERENCE*/,
		NULL,
		flags,
		&featureLevel,
		1,
		D3D11_SDK_VERSION,
		&sd,
		&sc,
		&device,
		NULL,
		&ctx));

	m_device           = ID3D11DevicePtr(device);
	m_swapChain        = IDXGISwapChainPtr(sc);
	m_immediateContext = D3D11DeviceContext(ID3D11DeviceContextPtr(ctx));

	m_logger->log_info(QString("Successfully created D3D11 device: supports %1 features").arg(featureLevelName(featureLevel)));

	//
	// View creation
	//

	ID3D11Texture2D* pBackBuffer;
	// Get a pointer to the back buffer
	D3D11_API(m_swapChain->GetBuffer(0, __uuidof( ID3D11Texture2D ), (LPVOID*)&pBackBuffer));
	m_backBuffer = ID3D11Texture2DPtr(pBackBuffer);

	// Create a render-target view
	ID3D11RenderTargetView* rtv = NULL;
	D3D11_API(m_device->CreateRenderTargetView(m_backBuffer.get(), NULL, &rtv));
	m_renderTargetView = ID3D11RenderTargetViewPtr(rtv);

	// Bind both viewsx
	ID3D11RenderTargetView* view = m_renderTargetView.get();
	m_immediateContext.OMSetRenderTargets(1, &view, NULL);


	//
	// Viewport setup
	//

	// Setup the viewport
	D3D11_VIEWPORT vp;
	vp.Width = info.displayMode.width;
	vp.Height = info.displayMode.height;
	vp.MinDepth = 0;
	vp.MaxDepth = 1;
	vp.TopLeftX = 0;
	vp.TopLeftY = 0;
	m_immediateContext.RSSetViewports( 1, &vp );

	m_logger->log_debug("video device completely initialized");
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::D3D11Device::~D3D11Device()
{
	// Remove all render nodes, so that the code below will work
	// Otherwise there could be dangling effect and/or mesh pointers
	m_renderNodes.clear();

	if(m_meshes.size())
		m_logger->log_error("Please release all meshes before destroying the engine");

	if(m_effects.size())
		m_logger->log_error("Please release all effects before destroying the engine");

	if(m_textures.size())
		m_logger->log_error("Please release all textures before destroying the engine");
}
///////////////////////////////////////////////////////////////////////////////////////////////////




QString Engine::Video::D3D11Device::apiName() const
{
	return "D3D11";
}
///////////////////////////////////////////////////////////////////////////////////////////////////




bool Engine::Video::D3D11Device::setDisplayMode(const DisplayMode& displayMode)
{
	return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::resize(uint32 width, uint32 height)
{
	m_info.displayMode.width = width;
	m_info.displayMode.height = height;

	DXGI_SWAP_CHAIN_DESC desc = createDeviceDesc();
	m_renderTargetView.release();
	m_backBuffer.release();
	D3D11_API(m_swapChain->ResizeBuffers(desc.BufferCount, desc.BufferDesc.Width, desc.BufferDesc.Height, desc.BufferDesc.Format, desc.Flags));

	ID3D11Texture2D* pBackBuffer;
	// Get a pointer to the back buffer
	D3D11_API(m_swapChain->GetBuffer(0, __uuidof( ID3D11Texture2D ), (LPVOID*)&pBackBuffer));
	m_backBuffer = ID3D11Texture2DPtr(pBackBuffer);

	// Create a render-target view
	ID3D11RenderTargetView* rtv = NULL;
	D3D11_API(m_device->CreateRenderTargetView(pBackBuffer, NULL, &rtv));
	m_renderTargetView = ID3D11RenderTargetViewPtr(rtv);

	// Bind both viewsx
	ID3D11RenderTargetView* view = m_renderTargetView.get();
	m_immediateContext.OMSetRenderTargets(1, &view, NULL);

	m_logger->log_debug(QString("Successfully resized to %1x%2 px").arg(width).arg(height));
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::changeWindow(HWND newWindow)
{
	m_info.window = newWindow;
	m_swapChain.release();

	DXGI_SWAP_CHAIN_DESC desc = createDeviceDesc();
	m_renderTargetView.release();
	m_backBuffer.release();
	IDXGISwapChain* swapChain = NULL;
	D3D11_API(m_factory->CreateSwapChain(m_device.get(), &desc, &swapChain));
	m_swapChain = IDXGISwapChainPtr(swapChain);

	ID3D11Texture2D* pBackBuffer;
	// Get a pointer to the back buffer
	D3D11_API(m_swapChain->GetBuffer(0, __uuidof( ID3D11Texture2D ), (LPVOID*)&pBackBuffer));
	m_backBuffer = ID3D11Texture2DPtr(pBackBuffer);

	// Create a render-target view
	ID3D11RenderTargetView* rtv = NULL;
	D3D11_API(m_device->CreateRenderTargetView(pBackBuffer, NULL, &rtv));
	m_renderTargetView = ID3D11RenderTargetViewPtr(rtv);

	// Bind both viewsx
	ID3D11RenderTargetView* view = m_renderTargetView.get();
	m_immediateContext.OMSetRenderTargets(1, &view, NULL);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

DisplayMode Engine::Video::D3D11Device::displayMode() const
{
	return m_displayMode;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

intsize2 Engine::Video::D3D11Device::size() const
{
	return intsize2(m_displayMode.width, m_displayMode.height);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

uint32 Engine::Video::D3D11Device::width() const
{
	return m_displayMode.width;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

uint32 Engine::Video::D3D11Device::height() const
{
	return m_displayMode.height;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

float Engine::Video::D3D11Device::aspect() const
{
	return m_displayMode.aspect;
}
///////////////////////////////////////////////////////////////////////////////////////////////////




VideoAdapters Engine::Video::D3D11Device::adapters()
{
	return queryAdapters();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

VideoAdapter Engine::Video::D3D11Device::currentAdapter()
{
	return m_videoAdapter;
}
///////////////////////////////////////////////////////////////////////////////////////////////////




Engine::Video::Mesh::ptr Engine::Video::D3D11Device::createMesh(uint32 sizePerVertex, uint32 sizePerIndex, const StaticReflection& vertexReflection, Format::Type indexType, Primitive::Type primitiveType, Mesh::Type meshType)
{
	auto mesh = D3D11Mesh::create(boost::bind(&D3D11Device::meshDestroyed, this, _1),
		m_device,
		m_immediateContext,
		sizePerVertex,
		sizePerIndex,
		vertexReflection,
		indexType,
		primitiveType,
		meshType);

	m_meshes.insert(mesh);

	return mesh;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::meshDestroyed(D3D11Mesh::weak_ptr mesh)
{
	Meshes::iterator i = m_meshes.find(mesh);
	if(i == m_meshes.end())
		BOOSTEXT_THROW(programming_error("Mesh does not exist"));

	m_meshes.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::Texture::ptr Engine::Video::D3D11Device::createTexture(const QByteArray& data)
{
	auto texture = D3D11Texture::create(boost::bind(&D3D11Device::textureDestroyed, this, _1), data, m_device);
	m_textures.insert(texture);
	return texture;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::textureDestroyed(D3D11Texture::weak_ptr texture)
{
	Textures::iterator i = m_textures.find(texture);
	if(i == m_textures.end())
		BOOSTEXT_THROW(programming_error("Texture does not exist"));

	m_textures.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::Effect::ptr Engine::Video::D3D11Device::createEffect(const QString& name)
{
	auto effect = D3D11Effect::create(boost::bind(&D3D11Device::effectDestroyed, this, _1), m_device, name);
	m_effects.insert(effect);
	return effect;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::effectDestroyed(D3D11Effect::weak_ptr effect)
{
	Effects::iterator i = m_effects.find(effect);
	if(i == m_effects.end())
		BOOSTEXT_THROW(programming_error("Effect does not exist"));

	m_effects.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::ConstantsBuffer::ptr Engine::Video::D3D11Device::createConstantsBuffer(uint32 size, const StaticReflection& bufferReflection, Buffer::Type bufferType)
{
	auto buffer = D3D11ConstantsBuffer::create(boost::bind(&D3D11Device::constantsBufferDestroyed, this, _1), size, bufferReflection, bufferType);
	m_constants.insert(buffer);
	return buffer;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::constantsBufferDestroyed(D3D11ConstantsBuffer::weak_ptr buffer)
{
	Constants::iterator i = m_constants.find(buffer);
	if(i == m_constants.end())
		BOOSTEXT_THROW(programming_error("ConstantBuffer does not exist"));

	m_constants.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::SamplerState::ptr Engine::Video::D3D11Device::createSamplerState()
{
	auto sampler = D3D11SamplerState::create(boost::bind(&D3D11Device::samplerStateDestroyed, this, _1), m_device);
	m_samplers.insert(sampler);
	return sampler;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::samplerStateDestroyed(D3D11SamplerState::weak_ptr sampler)
{
	Samplers::iterator i = m_samplers.find(sampler);
	if(i == m_samplers.end())
		BOOSTEXT_THROW(programming_error("SamplerState does not exist"));

	m_samplers.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::RasterizerState::ptr Engine::Video::D3D11Device::createRasterizerState()
{
	auto state = D3D11RasterizerState::create(boost::bind(&D3D11Device::rasterizerStateDestroyed, this, _1));
	m_rasterizerStates.insert(state);
	return state;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::rasterizerStateDestroyed(D3D11RasterizerState::weak_ptr state)
{
	RasterizerStates::iterator i = m_rasterizerStates.find(state);
	if(i == m_rasterizerStates.end())
		BOOSTEXT_THROW(programming_error("RasterizerState does not exist"));

	m_rasterizerStates.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Engine::Video::BlendState::ptr Engine::Video::D3D11Device::createBlendState()
{
	auto state = D3D11BlendState::create(boost::bind(&D3D11Device::blendStateDestroyed, this, _1));
	m_blendStates.insert(state);
	return state;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::blendStateDestroyed(D3D11BlendState::weak_ptr state)
{
	BlendStates::iterator i = m_blendStates.find(state);
	if(i == m_blendStates.end())
		BOOSTEXT_THROW(programming_error("BlendState does not exist"));

	m_blendStates.erase(i);
}
///////////////////////////////////////////////////////////////////////////////////////////////////




Engine::Video::Camera* Engine::Video::D3D11Device::camera()
{
	return &m_camera;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::render(const RenderNode::ptr& node)
{
	m_renderNodes.push_back(node);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Engine::Video::D3D11Device::update()
{
	m_immediateContext.Clear();
	m_camera.update();

	float4 clearColor(0.176f, 0.196f, 0.667f, 0.0f);
	m_immediateContext.ClearRenderTargetView(m_renderTargetView, clearColor);



	// Opaque elements are rendered from front to back in order
	// to rule out as many pixels as possible with the z-buffer-fail method

	// Transparent elements are rendered from back to front in order
	// to achieve the wanted visual effect and hide visual artifacts
	// produced by unordered transparent-rendering

	// All this is taken into account when rendering nodes:
	// We use stable sorting, so the user still has control
	// over drawing, if several nodes have an equal sorting result
	// (Like when drawing background at infinite range: the user
	// might want to play with the zbuffer and really ensure what
	// is drawn before what...)
	std::stable_sort(m_renderNodes.begin(), m_renderNodes.end(), &RenderNode::sort_ptr);

	foreach(const RenderNode::ptr &node, m_renderNodes)
	{
		D3D11Effect::ptr effect = boost::shared_polymorphic_cast<D3D11Effect>(node->effect);
		effect->draw(m_immediateContext, node);
	}

	m_renderNodes.clear();


	//
	// Presentation
	//

	HRESULT hr = m_swapChain->Present(0, 0);
	switch(hr)
	{
	case DXGI_ERROR_DEVICE_RESET: // TODO: Evaluate if the device should be reset and recreated
	default: D3D11_API_F(hr, m_swapChain->Present(0, 0));
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////




VideoAdapters Engine::Video::D3D11Device::queryAdapters()
{
	VideoAdapters adapters;

	// Create a DXGIFactory object.
	IDXGIFactory* f;
	D3D11_API(CreateDXGIFactory(__uuidof(IDXGIFactory) ,(void**)&f));
	IDXGIFactoryPtr factory(f);

	IDXGIAdapter* a;
	for(uint32 i = 0; factory->EnumAdapters(i, &a) != DXGI_ERROR_NOT_FOUND; ++i)
	{
		IDXGIAdapterPtr adapter(a);

		DXGI_ADAPTER_DESC desc;
		D3D11_API(adapter->GetDesc(&desc));
		adapters.push_back(VideoAdapter(desc.DeviceId, desc.VendorId, i));
		VideoAdapter& lastAdapter = adapters.last();

		IDXGIOutput* o;
		for(uint32 i = 0; adapter->EnumOutputs(i, &o) != DXGI_ERROR_NOT_FOUND; ++i)
		{
			IDXGIOutputPtr output(o);

			uint32 num = 0;
			DXGI_FORMAT format = DXGI_FORMAT_R8G8B8A8_UNORM;

			D3D11_API(output->GetDisplayModeList(format, 0, &num, 0));

			boost::shared_array<DXGI_MODE_DESC> descs(new DXGI_MODE_DESC[num]);
			D3D11_API(output->GetDisplayModeList(format, 0, &num, descs.get()));

			for(uint32 i = 0; i < num; ++i)
			{
				const DXGI_MODE_DESC& desc = descs[i];
				lastAdapter.modes.push_back(DisplayMode(desc.Width,
					desc.Height,
					WindowMode::Fullscreen,
					std::make_pair(desc.RefreshRate.Numerator, desc.RefreshRate.Denominator)));
			}
		}
	}

	return adapters;
}
///////////////////////////////////////////////////////////////////////////////////////////////////




IDXGIFactoryPtr Engine::Video::D3D11Device::createFactory()
{
	IDXGIFactory* f;
	D3D11_API(CreateDXGIFactory(__uuidof(IDXGIFactory) ,(void**)&f));
	return IDXGIFactoryPtr(f);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

IDXGIAdapterPtr Engine::Video::D3D11Device::adapterById(const IDXGIFactoryPtr& factory, uint32 id)
{
	IDXGIAdapter* a;
	D3D11_API(factory->EnumAdapters(id, &a));
	return IDXGIAdapterPtr(a);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

DXGI_SWAP_CHAIN_DESC Engine::Video::D3D11Device::createDeviceDesc() const
{
	DXGI_SWAP_CHAIN_DESC sd;
	ZeroMemory(&sd, sizeof(sd));

	sd.BufferCount  = 2;
	sd.BufferDesc.Height = m_info.displayMode.height;
	sd.BufferDesc.Width  = m_info.displayMode.width;
	sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
	sd.BufferDesc.RefreshRate.Numerator = m_info.displayMode.refreshRate.first;
	sd.BufferDesc.RefreshRate.Denominator = m_info.displayMode.refreshRate.second;
	sd.BufferUsage  = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	sd.SwapEffect   = DXGI_SWAP_EFFECT_DISCARD;
	sd.OutputWindow = m_info.window;
	// "The default sampler mode, with no anti-aliasing, has a count of 1 and a quality level of 0." - MSDN
	sd.SampleDesc.Count   = 1;
	sd.SampleDesc.Quality = 0;
	sd.Flags     = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
	sd.Windowed  = (m_info.displayMode.windowMode == WindowMode::Windowed) ? TRUE : FALSE;

	return sd;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

uint32 Engine::Video::D3D11Device::deviceFlags()
{
	uint32 flags = D3D11_CREATE_DEVICE_SINGLETHREADED;
	#ifdef _DEBUG
		flags |= D3D11_CREATE_DEVICE_DEBUG;
	#endif

	return flags;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

D3D_FEATURE_LEVEL Engine::Video::D3D11Device::highestFeatureLevel(const VideoAdapter& videoAdapter) const
{
	IDXGIAdapterPtr adapter = adapterById(m_factory, videoAdapter.index);

	D3D_FEATURE_LEVEL featureLevel;
	D3D11_API(D3D11CreateDevice(adapter.get(),
		D3D_DRIVER_TYPE_UNKNOWN,
		NULL,
		NULL,
		NULL,
		0,
		D3D11_SDK_VERSION,
		NULL,
		&featureLevel,
		NULL));

	return featureLevel;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

const char* Engine::Video::D3D11Device::featureLevelName(D3D_FEATURE_LEVEL level)
{
	switch(level)
	{
		case D3D_FEATURE_LEVEL_9_1:  return "D3D 9.1";
		case D3D_FEATURE_LEVEL_9_2:  return "D3D 9.2";
		case D3D_FEATURE_LEVEL_9_3:  return "D3D 9.3";
		case D3D_FEATURE_LEVEL_10_0: return "D3D 10.0";
		case D3D_FEATURE_LEVEL_10_1: return "D3D 10.1";
		case D3D_FEATURE_LEVEL_11_0: return "D3D 11.0";
		default:                     return "Unknown feature level";
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
